""" 
@Time ： 2025/8/26 14:45
@Auth ： zh
@File ：test3.py

功能：获取项目所有主材的合价总和
实现逻辑：通过调用三个API接口，分步获取数据，最终计算符合条件的合价总和
"""

# 导入所需库
import requests  # 用于发送HTTP请求
import json      # 用于处理JSON数据
import time      # 用于请求重试延迟
import logging   # 用于日志系统
import os        # 用于环境变量
from collections import defaultdict  # 用于记录(billId, code)的匹配次数，方便嵌套字典计数
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

# 配置日志系统
logging.basicConfig(
    level=logging.DEBUG,  # 修改为DEBUG级别
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("api_requests.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 配置管理
class Config:
    # 从环境变量获取配置，没有则使用默认值
    URL1 = os.getenv("API_URL1", "https://test.icii.agtsci.cn/api/cis/quota/subBill/library/listSubBill")
    URL2 = os.getenv("API_URL2", "https://test.icii.agtsci.cn/api/cis/quota/subBill/library/findBySubBillId")
    URL3 = os.getenv("API_URL3", "https://test.icii.agtsci.cn/api/cis/quota/collect/bill/billDetail")
    AUTH_TOKEN = os.getenv("API_AUTH_TOKEN", "bearer 513c45c2-2821-4787-8ac1-c72ab3ae7d2f")
    MAX_RETRIES = int(os.getenv("API_MAX_RETRIES", "3"))
    RETRY_DELAY = float(os.getenv("API_RETRY_DELAY", "2"))
    TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
    PAGE_SIZE = int(os.getenv("API_PAGE_SIZE", "100"))

# 请求头配置，根据实际接口要求可能需要调整
headers = {
    "Content-Type": "application/json",  # 声明请求体为JSON格式
    "Authorization": Config.AUTH_TOKEN   # 认证信息
}


class ApiCache:
    """简单的API请求缓存类"""
    def __init__(self):
        self.cache = {}
    
    def get(self, key):
        return self.cache.get(key)
    
    def set(self, key, value):
        self.cache[key] = value
    
    def clear(self):
        self.cache.clear()


# 创建缓存实例
api_cache = ApiCache()


def safe_float(value, default=0.0):
    """安全地将值转换为浮点数"""
    try:
        return float(value) if value is not None else default
    except (ValueError, TypeError):
        logger.warning(f"无法转换为浮点数: {value}")
        return default


def get_api_data(url, params=None, use_cache=False):
    """
    发送POST请求并返回JSON数据，带重试机制

    参数:
        url (str): 请求的API地址
        params (dict, optional): 请求参数（JSON格式）。默认为None
        use_cache (bool, optional): 是否使用缓存。默认为False

    返回:
        dict: 接口返回的JSON数据（解析后的字典），若请求失败或解析错误则返回None
    """
    # 生成缓存键
    cache_key = f"{url}:{json.dumps(params, sort_keys=True) if params else ''}"
    
    # 检查缓存
    if use_cache:
        cached_result = api_cache.get(cache_key)
        if cached_result is not None:
            logger.debug(f"使用缓存数据: {cache_key}")
            return cached_result
    
    # 创建session并配置重试策略
    session = requests.Session()
    retry = Retry(
        total=Config.MAX_RETRIES,
        backoff_factor=Config.RETRY_DELAY,
        status_forcelist=[500, 502, 503, 504]
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    
    try:
        response = session.post(url, headers=headers, json=params, timeout=Config.TIMEOUT)
        response.raise_for_status()
        result = response.json()
        
        # 存入缓存
        if use_cache:
            api_cache.set(cache_key, result)
        
        return result
    except requests.exceptions.RequestException as e:
        logger.error(f"请求错误 (尝试了{Config.MAX_RETRIES}次): {e}")
        return None
    except json.JSONDecodeError:
        logger.error("无法解析JSON响应")
        return None


def extract_first_total_cost(node, target_code):
    """
    递归提取第一个匹配目标code的节点的totalCost值，找到后立即返回

    参数:
        node (dict): 要遍历的节点（字典类型，可能包含children子节点）
        target_code (str): 要匹配的code值

    返回:
        float: 匹配节点的totalCost值（转换为浮点数），若未找到或转换失败则返回None
    """
    # 检查当前节点是否匹配目标code
    if node.get("code") == target_code:
        # 尝试提取totalCost并转换为浮点数
        return safe_float(node.get("totalCost"))

    # 递归遍历当前节点的children（若存在且为列表）
    children = node.get("children")
    if children and isinstance(children, list):
        for child in children:
            result = extract_first_total_cost(child, target_code)
            if result is not None:  # 找到第一个匹配项后立即返回，不再继续查找
                return result
    return None  # 未找到匹配项


def get_total_items(base_params):
    """获取总数据量"""
    logger.info("获取总数据量...")
    first_page_params = base_params.copy()
    first_page_params["pageNumber"] = 1
    response_first = get_api_data(Config.URL1, first_page_params)
    
    if not response_first or response_first.get("code") != 200:
        logger.error("获取总数据量失败")
        return 0
    
    return int(response_first.get("data", {}).get("total", 0))


def fetch_data_page(page_number, base_params):
    """获取指定页的数据"""
    logger.info(f"处理第 {page_number} 页数据")
    current_params = base_params.copy()
    current_params["pageNumber"] = page_number
    
    response = get_api_data(Config.URL1, current_params)
    if not response or response.get("code") != 200:
        logger.warning(f"获取第 {page_number} 页数据失败")
        return []
    
    return response.get("data", {}).get("list", [])


def extract_target_code(item_id):
    """从item_id提取目标code"""
    params = {"id": item_id}
    response = get_api_data(Config.URL2, params, use_cache=True)
    
    if not response or response.get("code") != 200:
        logger.warning(f"获取item_id={item_id}的code失败")
        return None
    
    return response.get("data", {}).get("code")


def get_assemble_price_info(bill_id):
    """获取合价信息"""
    params = {"billId": bill_id}
    response = get_api_data(Config.URL3, params, use_cache=True)
    
    if not response or response.get("code") != 200:
        logger.warning(f"获取bill_id={bill_id}的合价信息失败")
        return []
    
    return response.get("data", {}).get("assemblePriceInfos", [])


def extract_total_cost(assemble_price_infos, target_code, current_count, bill_id, bill_code_counter):
    """提取totalCost值"""
    if not assemble_price_infos:
        logger.warning("assemblePriceInfos列表为空")
        return None
    
    first_item = assemble_price_infos[0]
    children_list = first_item.get("children", [])
    logger.debug(f"第一层children总数: {len(children_list)}，当前需匹配第 {current_count} 个")
    
    current_code_count = bill_code_counter[bill_id][target_code]
    
    # 步骤1：在首层children中收集所有匹配target_code的节点
    first_level_matches = [child for child in children_list if child.get("code") == target_code]
    logger.debug(f"首层匹配code {target_code} 的节点数量: {len(first_level_matches)}")
    
    # 若首层匹配节点数量足够当前所需次数，则直接提取
    if len(first_level_matches) > current_code_count:
        target_child = first_level_matches[current_code_count]
        current_total_cost = safe_float(target_child.get("totalCost"))
        if current_total_cost is not None:
            logger.debug(f"在首层匹配节点中找到第 {current_code_count + 1} 个，提取totalCost: {current_total_cost}")
            bill_code_counter[bill_id][target_code] += 1
            return current_total_cost
    
    # 步骤2：若首层匹配节点不足，检查对应第一层children的第二层children
    if len(children_list) >= current_count:
        target_first_child = children_list[current_count - 1]
        logger.debug(f"根据处理次数定位到第一层children第 {current_count} 个元素（code: {target_first_child.get('code')}）")
        
        second_children = target_first_child.get("children", [])
        for second_child in second_children:
            if second_child.get("code") == target_code:
                current_total_cost = safe_float(second_child.get("totalCost"))
                if current_total_cost is not None:
                    logger.debug(f"在第二层children中找到匹配code的节点，提取totalCost: {current_total_cost}")
                    bill_code_counter[bill_id][target_code] += 1
                    return current_total_cost
                break
    else:
        logger.warning(f"第一层children数量不足（共{len(children_list)}个），无法匹配第{current_count}个元素")
    
    # 步骤3：过滤纯数字code后按次数匹配
    logger.debug(f"前两层均无匹配，过滤纯数字code后按处理次数查找第 {current_count} 个元素")
    filtered_children = [child for child in children_list if not child.get("code", "").isdigit()]
    logger.debug(f"过滤后children列表长度：{len(filtered_children)}（原长度：{len(children_list)}）")
    
    if len(filtered_children) >= current_count:
        target_child = filtered_children[current_count - 1]
        current_total_cost = extract_first_total_cost(target_child, target_code)
        if current_total_cost is not None:
            logger.debug(f"通过递归找到totalCost: {current_total_cost}")
            bill_code_counter[bill_id][target_code] += 1
            return current_total_cost
        else:
            logger.warning("递归未找到有效totalCost")
    else:
        logger.warning(f"过滤后children列表长度不足，无法获取第 {current_count} 个元素")
    
    return None


def process_item(item, processed_count, total_cycles, bill_id_counter, bill_code_counter):
    """处理单个条目并返回totalCost"""
    logger.info(f"处理第 {processed_count}/{total_cycles} 条数据")
    
    bill_id = item.get("billId")
    item_id = item.get("id")
    
    if not bill_id or not item_id:
        logger.warning("缺少billId或id，跳过此条数据")
        return None
    
    logger.debug(f"提取到 billId: {bill_id}, id: {item_id}")
    
    # 计算当前billId的处理次数
    current_count = bill_id_counter.get(bill_id, 0) + 1
    bill_id_counter[bill_id] = current_count
    logger.debug(f"当前是billId {bill_id} 的第 {current_count} 次处理")
    
    # 获取目标code
    target_code = extract_target_code(item_id)
    if not target_code:
        logger.warning("未找到目标code，跳过此条数据")
        return None
    
    logger.debug(f"提取到目标code: {target_code}")
    
    # 获取合价信息
    assemble_price_infos = get_assemble_price_info(bill_id)
    
    # 提取totalCost
    return extract_total_cost(assemble_price_infos, target_code, current_count, bill_id, bill_code_counter)


def main():
    """主函数：协调各步骤执行，实现数据获取、处理和结果计算"""
    # 基础请求参数，pageNumber会在分页处理时动态调整
    base_params = {
        "name": "",
        "billName": "",
        "billDesc": "",
        "saveParam": "0",
        "orderFields": [],
        "type": "1",
        "paramJsonStr": ["测试子清单库"],
        "quotaIds": ["7365944226045100032"],
        "pageParamJson": {
            "paramJsonStr": ["测试子清单库"],
            "quotaIds": ["7365944226045100032"],
            "saveParam": 0
        },
        "fileKindList": [],
        "regionIdList": [],
        "projectCategoryIds": [],
        "startTime": "",
        "endTime": "",
        "pageSize": Config.PAGE_SIZE  # 使用配置中的页大小
    }

    # 存储提取到的所有有效totalCost值
    total_cost_list = []
    # 记录每个billId的处理次数（用于定位children列表中的索引）
    bill_id_counter = {}
    # 记录每个(billId, code)组合的匹配次数
    bill_code_counter = defaultdict(lambda: defaultdict(int))
    page_number = 1  # 起始页码，从1开始
    total_cycles = get_total_items(base_params)  # 总数据量
    processed_count = 0  # 已处理的数据条数

    if total_cycles == 0:
        logger.error("没有数据需要处理，程序退出")
        return
    
    logger.info(f"共需处理 {total_cycles} 条数据")

    # 分页处理所有数据
    while processed_count < total_cycles:
        # 获取当前页数据
        list_items = fetch_data_page(page_number, base_params)
        current_page_size = len(list_items)
        
        if current_page_size == 0:
            logger.warning("当前页无数据，结束处理")
            break
        
        # 处理当前页的每条数据
        for item in list_items:
            processed_count += 1
            total_cost = process_item(item, processed_count, total_cycles, 
                                     bill_id_counter, bill_code_counter)
            
            if total_cost is not None:
                total_cost_list.append(total_cost)
        
        # 准备处理下一页
        page_number += 1

    # 输出最终结果
    logger.info("=" * 50)
    logger.info(f"处理完成，共提取 {len(total_cost_list)} 个有效totalCost值")
    logger.info(f"totalCost列表: {total_cost_list}")  # 保留totalCost列表输出
    logger.info(f"totalCost总和: {sum(total_cost_list) if total_cost_list else 0}")


# 程序入口：当脚本直接运行时执行main函数
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        logger.info("程序被用户中断")
    except Exception as e:
        logger.error(f"程序运行出错: {e}", exc_info=True)